package net.liuyimin.service;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import net.liuyimin.entity.StockEntity;
import net.liuyimin.mapper.StockMapper;
//import org.springframework.context.annotation.Scope;
//import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

@Service
//@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)  //Spring容器默认这个类是单例的（singleton），此处将指定为多例模式（prototype），每次请求都会创建一个新的实例
public class StockService {

    @Resource
    private StockMapper stockMapper;

    /**
     * 演示共享资源在MySQL中在高并发下的超卖问题
     */
    public void deduct() {
        //1.查询库存
        StockEntity stockEntity = stockMapper.selectOne(
                Wrappers.<StockEntity>lambdaQuery().eq(StockEntity::getProductCode, "1001")
        );
        //2.判断库存是否充足
        if (stockEntity != null && stockEntity.getCount() > 0)  {
            stockEntity.setCount(stockEntity.getCount() - 1);
            //3.更新库存到数据库
            stockMapper.updateById(stockEntity);
        }
    }

    /**
     * JVM本地锁 : 使用 synchronized 关键字解决超卖现象
     */
    public synchronized void deductBySynchronized() {
        //1.查询库存
        StockEntity stockEntity = stockMapper.selectOne(
                Wrappers.<StockEntity>lambdaQuery().eq(StockEntity::getProductCode, "1001")
        );
        //2.判断库存是否充足
        if (stockEntity != null && stockEntity.getCount() > 0)  {
            stockEntity.setCount(stockEntity.getCount() - 1);
            //3.更新库存到数据库
            stockMapper.updateById(stockEntity);
        }
    }

    /**
     * JVM本地锁 : 使用 ReentrantLock 可重入锁解决超卖现象
     */
    private ReentrantLock reentrantLock = new ReentrantLock();

    //@Transactional
    public void deductByReentrantLock() {
        reentrantLock.lock();
        try {
            //1.查询库存
            StockEntity stockEntity = stockMapper.selectOne(
                    Wrappers.<StockEntity>lambdaQuery().eq(StockEntity::getProductCode, "1001")
            );
            //2.判断库存是否充足
            if (stockEntity != null && stockEntity.getCount() > 0)  {
                stockEntity.setCount(stockEntity.getCount() - 1);
                //3.更新库存到数据库
                stockMapper.updateById(stockEntity);
            }
        } finally {
            reentrantLock.unlock();
        }
    }

    /**
     * MySQL锁机制 : 使用一条SQL语句解决超卖问题
     */
    @Transactional
    public void deductByOneSql() {
        this.stockMapper.updateStock("1001", 1);
    }

    /**
     * MySQL锁机制 : 使用SelectForUpdate悲观锁解决超卖问题
     */
    @Transactional
    public void deductBySelectForUpdate() {
        // 1.查询库存信息并锁定库存信息
        List<StockEntity> stockEntityList = this.stockMapper.selectStockForUpdate("1001");
        //说明：这里取第一个库存模拟业务，实际上真实业务在选库存的时候，需要根据库存数量、库存优先级等选择合适的库存
        StockEntity stockEntity = stockEntityList.get(0);

        // 2.判断库存是否充足
        if (stockEntity != null && stockEntity.getCount() > 0) {
            // 3.扣减库存,更新库存到数据库
            stockEntity.setCount(stockEntity.getCount() - 1);
            stockMapper.updateById(stockEntity);
        }
    }

    /**
     * MySQL锁机制 : 借助version乐观锁解决超卖问题
     */
    //@Transactional   //数据库连接池连接超时是事务注解导致的，相当于一个手动事务，需要方法执行完才能释放。执行 MDL 操作，上锁，形成阻塞，导致连接超时（默认 30S 超时）。解决办法就是不要加事务注解了。
    public void deductByCasVersion() {
        // 1.查询库存信息并锁定库存信息
        List<StockEntity> stockEntityList = this.stockMapper.selectList(
                Wrappers.<StockEntity>lambdaQuery().eq(StockEntity::getProductCode, "1001")
        );
        //说明：这里取第一个库存模拟业务，实际上真实业务在选库存的时候，需要根据库存数量、库存优先级等选择合适的库存
        StockEntity stockEntity = stockEntityList.get(0);
        
        // 2.判断库存是否充足
        if (stockEntity != null && stockEntity.getCount() > 0) {
            // 3.扣减库存,更新库存到数据库
            stockEntity.setCount(stockEntity.getCount() - 1);
            Integer version = stockEntity.getVersion();
            stockEntity.setVersion(version + 1);  //版本号+1
            // 更新之前先判断是否是之前查询的那个版本，如果不是重试
            if (stockMapper.update(stockEntity, Wrappers.<StockEntity>lambdaUpdate().eq(StockEntity::getId, stockEntity.getId()).eq(StockEntity::getVersion, version)) == 0) {
                // 递归调用（不停的重试）导致的问题：栈内存溢出，数据库连接池连接超时。避免栈内存溢出，睡一下，不要不停的重试。
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 如果更新失败,则进行重试!
                this.deductByCasVersion();
            }
        }
    }

}
